package com.tools.web.filter.gateway;

import com.tools.common.exception.IORuntimeException;
import com.tools.common.exception.InvalidOperationException;
import com.tools.common.io.CharsetEnum;
import com.tools.common.io.IOKit;
import com.tools.common.object.Note;
import com.tools.common.object.string.StrKit;
import com.tools.common.object.string.VerifyKit;
import com.tools.common.thread.TimePair;
import com.tools.common.thread.Timeunit;
import com.tools.db.ip.IPKit;
import com.tools.web.WebKit;
import com.tools.web.filter.cors.CorsFilter;
import com.tools.web.filter.cors.CorsSource;
import com.tools.web.interceptor.auth.AuthenticationManager;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * 网关过滤器
 * */
@Note("网关过滤器")
public abstract class GetawayFilter implements Filter {

    @Note("response 的 ContentType。默认为 JSON 字符串")
    private final String responseContentType;

    @Note("response 的默认字符集。默认为 UTF-8")
    private final String responseCharset;

    @Note("【代理服务器的 IP 白名单】 的 key。不为空则开启。" +
            "开启后必须要携带代理请求头，同时必须手动配置放行的代理服务器 IP，否则会拒绝一切请求。")
    private String proxyWhitesCacheKey;

    @Note("【接口禁用列表】 的 key。不为空则开启")
    private String disableURICacheKey;

    @Note("【客户端 IP 白名单】 的 key。不为空则开启")
    private String ipWhitesCacheKey;

    @Note("【客户端 IP 黑名单】 的 key。不为空则开启")
    private String ipBlacksCacheKey;

    @Note("【客户端 IP 黑名单】 功能启动后才生效。" +
            "在 pullBlackThresholdTime 的时间内访问次数大于大于 pullBlackThreshold 就拉黑客户端 IP。" +
            "pullBlackCacheKeyPrefix 是要将当前访问 IP 加入缓存时的 key 的前缀。" +
            "该功能在 【客户端 IP 黑名单】 功能关闭后失效，当 【客户端 IP 黑名单】 功能再开启后再以初始化时的配置参数同步开启。" +
            "AuthenticationManager 是是否在触发拉黑 IP 时判断该请求是否携带 token，若携带则拉黑 IP 之后继续封禁该用户的账户。" +
            "该功能在 useAuthHandler 为 true 且 authHandler 不为 null 时开启，同样的若 【客户端 IP 黑名单】 功能关闭后会同样失效，待黑名单功能开启时再同步生效。" +
            "useAuthHandler 主要是方便外部配置文件随时启用和关闭")
    private int pullBlacksThreshold;
    private long pullBlacksThresholdTime;
    private String pullBlacksThresholdTimeunitText;
    private String pullBlacksCacheKeyPrefix;
    private boolean useAuthHandler;
    private AuthenticationManager authHandler;

    @Note("公网 IP 存在 HttpServletRequest 域中的名称")
    public static final String PUBLIC_NET_IP_NAME = "publicNetIP";

    @Note("自定义的跨域过滤器。若网关开启跨域功能则会初始化并执行跨域功能。当然跨域过滤器该有的配置项在配置文件里不能少")
    private CorsFilter corsFilter;


    /* ********************************************************************************************
     *
     *         构造器
     *
     * *********************************************************************************************
     * */

    public GetawayFilter(GetawaySource source, boolean mustTrueCondition, String errorMessage) {
        if(!mustTrueCondition) throw new IllegalArgumentException(errorMessage);
        if(source == null) throw new NullPointerException("网关过滤器需要 GetawaySource 包装配置参数");
        //跨域设置
        CorsSource corsSource = source.getCorsSource();
        if(corsSource != null) {
            this.corsFilter = new CorsFilter(corsSource);
        }
        //contentType 设置
        String responseContentType = source.getResponseContentType();
        if(responseContentType == null || responseContentType.isEmpty()) this.responseContentType = "application/json";
        else this.responseContentType = responseContentType;
        //字符集设置
        CharsetEnum responseCharset = source.getResponseCharset();
        if(responseCharset == null) this.responseCharset = CharsetEnum.UTF_8.getValue();
        else this.responseCharset = responseCharset.getValue();
        Logger logger = protected_getLogger();
        //代理服务器 IP 白名单功能
        String proxyWhitesCacheKey = source.getProxyWhitesCacheKey();
        if(proxyWhitesCacheKey != null && !proxyWhitesCacheKey.isEmpty()) {
            this.proxyWhitesCacheKey = proxyWhitesCacheKey;
            logger.info("网关过滤器开启 【代理服务器 IP 白名单】 功能!");
            Set<String> proxyIPs = private_readInitDataAsSet(source.getProxyIPs(), source.getProxyIPsFilePath());
            if(proxyIPs.isEmpty()) {
                throw new InvalidOperationException("开启了 [代理服务器 IP 白名单] 功能，却没有配置放行的白名单代理服务器 IP，将拒绝接下来的所有的请求");
            } else {
                this.protected_putInitProxyWhitesData(proxyIPs);
                protected_logPrintInitDataList(logger, proxyIPs, "代理服务器 IP 白名单");
            }
        }
        //接口禁用列表功能
        String disableURICacheKey = source.getDisableURICacheKey();
        if(disableURICacheKey != null && !disableURICacheKey.isEmpty()) {
            this.disableURICacheKey = disableURICacheKey;
            logger.info("网关过滤器开启 【接口禁用列表】 功能!");
            Set<String> uris = private_readInitDataAsSet(source.getDisabledURIs(), source.getDisabledURIsFilePath());
            if(uris.isEmpty()) {
                logger.info("初始化禁用接口为空");
            } else {
                this.protected_putInitDisabledURIData(uris);
                protected_logPrintInitDataList(logger, uris, "已禁用的接口列表");
            }
        }
        //客户端 IP 白名单功能
        String ipWhitesCacheKey = source.getIpWhitesCacheKey();
        if(ipWhitesCacheKey != null && !ipWhitesCacheKey.isEmpty()) {
            this.ipWhitesCacheKey = ipWhitesCacheKey;
            logger.info("网关过滤器开启 【客户端 IP 白名单】 功能!");
            Set<String> whiteIPs = private_readInitDataAsSet(source.getWhitesIps(), source.getWhitesIpsFilePath());
            if(whiteIPs.isEmpty()) {
                logger.info("初始化客户端 IP 白名单为空");
            } else {
                this.protected_putInitIPWhitesData(whiteIPs);
                protected_logPrintInitDataList(logger, whiteIPs, "客户端 IP 白名单");
            }
        }
        //客户端 IP 黑名单功能
        String ipBlacksCacheKey = source.getIpBlacksCacheKey();
        if(ipBlacksCacheKey != null && !ipBlacksCacheKey.isEmpty()) {
            this.ipBlacksCacheKey = ipBlacksCacheKey;
            logger.info("网关过滤器开启 【客户端 IP 黑名单】 功能!");
            Set<String> blackIPs = private_readInitDataAsSet(source.getBlacksIps(), source.getBlacksIpsFilePath());
            if(blackIPs.isEmpty()) {
                logger.info("初始化客户端 IP 黑名单为空");
            } else {
                this.protected_putInitIPBlacksData(blackIPs);
                protected_logPrintInitDataList(logger, blackIPs, "客户端 IP 黑名单");
            }
            IPBlacksThreshold ipBlacksThreshold = source.getIpBlacksThreshold();
            if(ipBlacksThreshold != null && ipBlacksThreshold.isEnabled()) {
                this.pullBlacksThreshold = ipBlacksThreshold.getThreshold();
                this.pullBlacksCacheKeyPrefix = ipBlacksThreshold.getCacheKeyPrefix();
                TimePair thresholdTime = ipBlacksThreshold.getThresholdTime();
                Timeunit timeunit = this.protected_getImplSelfTimeunit();
                this.pullBlacksThresholdTimeunitText = timeunit.getText();
                long sourceTime = thresholdTime.getSourceTime();
                Timeunit sourceTimeunit = thresholdTime.getSourceTimeunit();
                this.pullBlacksThresholdTime = timeunit.cast(sourceTime, sourceTimeunit);
                logger.info("网关过滤器开启 【可动态拉黑客户端 IP 黑名单】 功能: {} - 在 {} {} 内访问大于等于 {} 次触发",
                        this.pullBlacksCacheKeyPrefix, sourceTime, sourceTimeunit, this.pullBlacksThreshold);
                if(ipBlacksThreshold.isEnabledDisabledUser()) {
                    this.useAuthHandler = ipBlacksThreshold.isUseAuthHandler();
                    this.authHandler = ipBlacksThreshold.getAuthHandler();
                    logger.info("检测到 AuthenticationManager 实例不为 null，同步开启 【动态封禁用户账户】 功能，在触发拉黑 IP 时同步触发。");
                }
            }
        }
    }


    /* ********************************************************************************************
     *
     *          自定义方法
     *
     * *********************************************************************************************
     * */

    @Note("【代理服务器的 IP 白名单】 功能是否开启。其 key 不为 null 就开启")
    public boolean proxyIsEnabled() {
        return proxyWhitesCacheKey != null;
    }

    @Note("【接口禁用列表】 功能是否开启。其 key 不为 null 就开启")
    public boolean disabledURIIsEnabled() {
        return disableURICacheKey != null;
    }

    @Note("【客户端 IP 白名单】 功能是否开启。其 key 不为 null 就开启")
    public boolean ipWhitesIsEnabled() {
        return ipWhitesCacheKey != null;
    }

    @Note("【客户端 IP 黑名单】 功能是否开启。其 key 不为 null 就开启")
    public boolean ipBlacksIsEnabled() {
        return ipBlacksCacheKey != null;
    }

    public String getProxyWhitesCacheKey() {
        return proxyWhitesCacheKey;
    }

    public String getDisableURICacheKey() {
        return disableURICacheKey;
    }

    public String getIpWhitesCacheKey() {
        return ipWhitesCacheKey;
    }

    public String getIpBlacksCacheKey() {
        return ipBlacksCacheKey;
    }

    /* ********************************************************************************************
     *
     *          子类必须实现的多态抽象方法
     *
     * *********************************************************************************************
     * */

    @Note("获取子类实现的日志输出实例")
    protected abstract Logger protected_getLogger();

    @Note("获取子类缓存默认存储数据的时间单位。如 Redis 是秒，本地缓存是毫秒。")
    protected abstract Timeunit protected_getImplSelfTimeunit();

    @Note("将所有代理服务器 IP 加入子类缓存相对应的容器列表里")
    protected abstract void protected_putInitProxyWhitesData(Set<String> proxyIps);

    @Note("将所有禁用的接口加入子类缓存相对应的容器列表里")
    protected abstract void protected_putInitDisabledURIData(Set<String> disabledURIs);

    @Note("将所有白名单的客户端 IP 加入子类缓存相对应的容器列表里")
    protected abstract void protected_putInitIPWhitesData(Set<String> ipWhites);

    @Note("将所有黑名单的客户端 IP 加入子类缓存相对应的容器列表里")
    protected abstract void protected_putInitIPBlacksData(Set<String> ipBlacks);

    @Note("从子类实现的缓存里获取该客户端 IP 已请求的次数")
    protected abstract Integer protected_getRequestedCount(String key);

    @Note("设置新的已请求次数到缓存里")
    protected abstract void protected_setRequestedCount(String key, int requestedCount, long expiration);

    @Note("在拉黑 IP 后手动移除记录已请求次数的条目")
    protected abstract void protected_removeRequestedCountEntry(String key);

    @Note("获取已请求次数的剩余过期时间")
    protected abstract long protected_getRemainder(String key);


    @Note("判断直接获取的 IP 是否在代理服务器 IP 白名单内")
    public abstract boolean isInProxyWhites(String remoteAddr);

    @Note("判断接口是否已被禁用")
    public abstract boolean isDisabledURI(String uri);

    @Note("判断客户端 IP 是否位于白名单内")
    public abstract boolean isWhitesIP(String clientIP);

    @Note("判断客户端 IP 是否位于黑名单内")
    public abstract boolean isBlacksIP(String clientIP);


    @Note("新增代理服务器 IP 到白名单内")
    public abstract void addInProxyIPs(String remoteAddr);

    @Note("将指定的代理服务器 IP 移出白名单")
    public abstract void removeFromProxyIPs(String remoteAddr);

    @Note("清空代理服务器 IP 白名单")
    public abstract void clearProxyIPs();

    @Note("复制代理服务器 IP 白名单")
    public abstract Set<String> copyProxyIPs();

    @Note("判断代理服务器 IP 白名单是否为空列表")
    public abstract boolean proxyIPsIsEmpty();

    @Note("调试日志打印输出代理服务器 IP 白名单")
    public abstract void debugProxyIPs();


    @Note("禁用指定的接口")
    public abstract void addInDisabledURIs(String uri);

    @Note("开放指定的接口")
    public abstract void removeFromDisabledURIs(String uri);

    @Note("清空已禁用的接口列表")
    public abstract void clearDisabledURIs();

    @Note("复制已禁用的接口列表")
    public abstract Set<String> copyDisabledURIs();

    @Note("判断已禁用的接口列表是否为空列表")
    public abstract boolean disabledURIsIsEmpty();

    @Note("调试日志打印输出已禁用的接口列表")
    public abstract void debugDisabledURIs();

    @Note("把客户端 IP 加入白名单")
    public abstract void addInWhites(String clientIP);

    @Note("把客户端 IP 从白名单中移除")
    public abstract void removeFromWhites(String clientIP);

    @Note("清空客户端 IP 白名单")
    public abstract void clearWhites();

    @Note("复制客户端 IP 白名单")
    public abstract Set<String> copyWhites();

    @Note("判断客户端 IP 白名单是否为空列表")
    public abstract boolean ipWhitesIsEmpty();

    @Note("调试日志打印输出客户端 IP 白名单")
    public abstract void debugIPWhites();

    @Note("把客户端 IP 加入黑名单")
    public abstract void addInBlacks(String clientIP);

    @Note("把客户端 IP 从黑名单中移除")
    public abstract void removeFromBlacks(String clientIP);

    @Note("清空客户端 IP 黑名单")
    public abstract void clearBlacks();

    @Note("复制客户端 IP 黑名单")
    public abstract Set<String> copyBlacks();

    @Note("判断客户端 IP 黑名单是否为空列表")
    public abstract boolean ipBlacksIsEmpty();

    @Note("调试日志打印输出客户端 IP 黑名单")
    public abstract void debugIPBlacks();


    /* ********************************************************************************************
     *
     *          拦截 + 处理 逻辑
     *
     * *********************************************************************************************
     * */

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        this.protected_getLogger().info("网关过滤器已启动并初始化完成！");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        if(this.corsFilter != null) {
            boolean b = this.corsFilter.preFiltering(request, response);
            if(b) return;
        }
        Logger logger = protected_getLogger();
        //设置响应字符集和 Content-Type
        response.setContentType(this.responseContentType);
        response.setCharacterEncoding(this.responseCharset);
        //判断是否开启了代理服务器 IP 白名单，并校验 remoteAddr 是否在白名单内，然后获取并储存客户端 IP 和它们的公网 IP
        if(private_proxyEnabledCheckHandle(logger, request, response)) {
            return;
        }
        //禁用接口判断
        String requestURI = request.getRequestURI();
        if(this.disabledURIIsEnabled() && this.isDisabledURI(requestURI)) {
            logger.error("接口 【{}】 已被禁用", requestURI);
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        //客户端 IP 白名单验证
        String clientIP = (String) request.getAttribute(WebKit.CLIENT_IP_NAME);
        if(this.ipWhitesIsEnabled() && !this.isWhitesIP(clientIP)) {
            logger.error("IP 【{}】 没有位于白名单内，拒绝访问", clientIP);
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            return;
        }
        //客户端 IP 黑名单验证
        if(this.ipBlacksIsEnabled()) {
            //是否已被拉黑
            if(this.isBlacksIP(clientIP)) {
                logger.error("IP 【{}】 位于黑名单内，拒绝访问", clientIP);
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                return;
            }
            //是否达到了拉黑的阈值，是则拉黑该客户端 IP
            if(private_checkIsAddBlacksAndDisabledAccount(clientIP, request, logger)) {
                logger.error("IP 【{}】 在 {} {} 内访问超过阈值 {} 次，已被加入黑名单",
                        clientIP, pullBlacksThresholdTime, pullBlacksThresholdTimeunitText, pullBlacksThreshold);
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                return;
            }
        }
        //放行请求
        filterChain.doFilter(request, response);
    }


    @Override
    public void destroy() {
        this.protected_getLogger().info("网关过滤器已关闭并销毁完毕！");
    }

    /* ********************************************************************************************
     *
     *         私有逻辑
     *
     * *********************************************************************************************
     * */


    @Note("将 str 以【,】切割，再读取 filePath 的文件里的内容，统一放入 HashSet 集合里返回")
    private Set<String> private_readInitDataAsSet(String str, String filePath) {
        Set<String> results = new HashSet<>();
        if(str != null && !str.isEmpty()) {
            String[] a = StrKit.split(str, ",");
            Collections.addAll(results, a);
        }
        if(filePath == null || filePath.isEmpty()) return results;
        File file = new File(filePath);
        BufferedReader reader = null;
        if(file.exists()) {
            try {
                String line;
                reader = IOKit.bufferedReader(file);
                while ((line = reader.readLine()) != null) {
                    if(line.startsWith("deny")) {
                        results.add(line.substring(4).trim());
                        continue;
                    }
                    if(line.startsWith("allow")) {
                        results.add(line.substring(5).trim());
                        continue;
                    }
                    results.add(line);
                }
            } catch (IOException e) {
                throw new IORuntimeException(e);
            } finally {
                IOKit.close(reader);
            }
        }
        return results;
    }


    @Note("日志输出初始化得到的数据集合")
    protected void protected_logPrintInitDataList(Logger logger, Set<String> data, String title) {
        String border = StrKit.LOG_PRINT_RADIUS_BORDER;
        StringBuilder builder = new StringBuilder();
        for (String s : data) {
            builder.append('\n').append(s);
        }
        logger.info("\n{} {} {}{}\n{}============={}\n",
                border, title, border, builder.toString(), border, border);
    }



    @Note("校验 remoteAddr 是否在代理服务器白名单内，并且获取客户端 IP 和公网 IP 放到 HttpServletRequest 域内储存")
    private boolean private_proxyEnabledCheckHandle(Logger logger, HttpServletRequest request, HttpServletResponse response) {
        //尝试获取来自客户端的公网 IP
        String numberIP = request.getHeader("Application-Time");
        String requestTime = request.getHeader("Request-Time");
        String publicNetIP = null;
        if(VerifyKit.isAllInteger(numberIP, requestTime)) {
            long numIP = Long.parseLong(numberIP) -  Long.parseLong(requestTime);
            publicNetIP = IPKit.toStringIp(numIP);
        }
        //尝试获取代理请求头
        String proxyHeader = WebKit.getProxyHeader(request);
        //如果开启了代理服务器 IP 白名单功能
        if(this.proxyIsEnabled()) {
            //那就必须要携带代理头，没有那就拒绝访问
            if(proxyHeader == null) {
                logger.error("直连白名单开启后，网关强制要求的代理 header 没有携带");
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                return true;
            }
            String remoteAddr = request.getRemoteAddr();
            if(!this.isInProxyWhites(remoteAddr)) {
                logger.error("代理服务器 IP remote address 【{}】 不在白名单内，拒绝访问", remoteAddr);
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                return true;
            }
            //获取代理头携带的公网 IP。与获取到的公网 IP 比较，不一致则采用代理头的作为客户端的真实 IP
            String clientIP = WebKit.buildClientIPByHeader(proxyHeader, request);
            request.setAttribute(WebKit.CLIENT_IP_NAME, clientIP);
            if(publicNetIP == null || publicNetIP.equals(clientIP)) request.setAttribute(PUBLIC_NET_IP_NAME, clientIP);
            else request.setAttribute(PUBLIC_NET_IP_NAME, publicNetIP);
            return false;
        }
        //不开代理服务器 IP 白名单的情况下
        String clientIpName = WebKit.CLIENT_IP_NAME;
        if(proxyHeader == null) {
            if(publicNetIP == null) {
                //客户端 IP 就采用本机，公网 IP 为空
                request.setAttribute(clientIpName, WebKit.buildClientIPByHeader(null, request));
                request.setAttribute(PUBLIC_NET_IP_NAME, "");
            } else {
                //客户端 IP 跟公网 IP一致。
                request.setAttribute(clientIpName, publicNetIP);
                request.setAttribute(PUBLIC_NET_IP_NAME, publicNetIP);
            }
            return false;
        }
        String clientIP = WebKit.buildClientIPByHeader(proxyHeader, request);
        if(publicNetIP == null) {
            request.setAttribute(PUBLIC_NET_IP_NAME, "");
            request.setAttribute(clientIpName, clientIP);
            return false;
        }
        if(publicNetIP.equals(clientIP)) {
            request.setAttribute(PUBLIC_NET_IP_NAME, clientIP);
            request.setAttribute(clientIpName, clientIP);
            return false;
        }
        request.setAttribute(PUBLIC_NET_IP_NAME, publicNetIP);
        request.setAttribute(clientIpName, clientIP);
        return false;
    }


    @Note("判断客户端 IP 是否已达到拉黑的阈值，是则拉黑 IP。" +
            "同时判断是否开启了封禁账户功能，若开启了且请求携带了 token，则执行封禁用户账户的逻辑")
    private boolean private_checkIsAddBlacksAndDisabledAccount(String clientIP, HttpServletRequest request, Logger logger) {
        //如果小于等于 0 那说明不做次数限制
        if(pullBlacksThreshold <= 0 || pullBlacksThresholdTime <= 0) {
            return false;
        }
        String key = this.pullBlacksCacheKeyPrefix + clientIP;

        //第一次请求
        Integer requestedCount = this.protected_getRequestedCount(key);
        if(requestedCount == null) {
            this.protected_setRequestedCount(key, 1, pullBlacksThresholdTime);
            return false;
        }
        //还没到阈值，次数自增后放行
        if(requestedCount < pullBlacksThreshold) {
            long ttl = this.protected_getRemainder(key);
            if(ttl <= 0) {
                this.protected_setRequestedCount(key, 1, pullBlacksThresholdTime);
            } else {
                this.protected_setRequestedCount(key, requestedCount + 1, ttl);
            }
            return false;
        }
        //已达到阈值，拉黑 IP + 封禁账户
        this.addInBlacks(clientIP);
        this.protected_removeRequestedCountEntry(key);
        if(this.useAuthHandler && this.authHandler != null) {
            this.authHandler.handleDisable(request);
        }
        return true;
    }
}
